/*
 * ChordDisplay.java
 *
 * Copyright (c) 2010 Jaymie Strecker (strecker at cs.umd.edu). All rights reserved.
 *
 * This file is part of Tonic.
 *
 * Tonic is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tonic is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tonic.  If not, see <http://www.gnu.org/licenses/>.
 */

package tonic.gui;

import javax.swing.*;
import java.util.*;
import tonic.audio.PitchAmplitude;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

/**
 * A ChordDisplay is a panel that contains a list of notes with visual cues showing each
 * note's amplitude, along with associated controls that customize the view. 
 *
 * @author Jaymie Strecker (strecker at cs.umd.edu)
 */
public class ChordDisplay extends JPanel {
    private JTextPane notesDisplay;
    private boolean separateOctaves;
    private boolean useFlats;
    Vector<PitchAmplitude> currPitchAmplitudes = null;
    private static final int MAX_FONT_SIZE = 60;
    private static final int DOT_FONT_SIZE = 24;
    private static final int MIN_FONT_SIZE = 12;
    private static final HashMap<String, Double> DEFAULT_TEXT;
    static {
        DEFAULT_TEXT = new HashMap<String, Double>();
        DEFAULT_TEXT.put(" ", new Double(1.0));
    }

    /**
     * Creates a ChordDisplay with empty text. 
     */
    public ChordDisplay() {
        super(new GridBagLayout());

        notesDisplay = new NoWrapTextPane();
        notesDisplay.setEditable(false);
        JScrollPane notesScrollPane = new JScrollPane(notesDisplay);
        notesScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        notesScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
        notesScrollPane.getViewport().setBackground(Color.WHITE);
        clear();

        separateOctaves = false;
        JToggleButton octaves = new JToggleButton("Combine octaves");
        octaves.setSelected(! separateOctaves);
        useFlats = false;
        JToggleButton flats = new JToggleButton("#");
        flats.setSelected(! useFlats);
        JPanel controlPanel = new JPanel(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 0;
        c.gridy = 0;
        controlPanel.add(octaves, c);
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 0;
        c.gridy = 1;
        controlPanel.add(flats, c);
        
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 1;
        c.gridy = 0;
        c.insets = new Insets(0, 5, 0, 5);
        add(controlPanel, c);
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 0;
        c.gridy = 0;
        c.insets = new Insets(0, 5, 0, 5);
        c.weightx = 1.0;
        c.weighty = 1.0;
        add(notesScrollPane, c);

        octaves.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                separateOctaves = ! separateOctaves;
                if (currPitchAmplitudes != null) {
                    display(currPitchAmplitudes);
                }
            }
        });
        flats.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                useFlats = ! useFlats;
                if (currPitchAmplitudes != null) {
                    display(currPitchAmplitudes); 
                }
            }
        }); 

        setBorder(BorderFactory.createLineBorder(Color.BLACK));
    }

    /**
     * Clears all text from the display. 
     */
    public void clear() {
        // Add invisible text so the display will get the desired height
        setDisplayText(notesDisplay, DEFAULT_TEXT);
        currPitchAmplitudes = null; 
    }

    /**
     * Displays the list of pitches in descending order of amplitude, according to the
     * current control settings.
     *
     * @param pitchAmplitudes   assumed to be in descending order of amplitude.
     */
    public void display(Vector<PitchAmplitude> pitchAmplitudes) {
        if (separateOctaves) {
            setDisplayText(notesDisplay, makePitchAmplitudes(pitchAmplitudes));
        } else {
            setDisplayText(notesDisplay, makeSemitoneAmplitudes(pitchAmplitudes));
        }
        notesDisplay.setCaretPosition(0);
        currPitchAmplitudes = pitchAmplitudes; 
    }

    /**
     * Converts a list of (pitch, amplitude) pairs to a set of (text, size) pairs,
     * combining all pitches of the same note at different octaves.
     *
     * @param pitchAmplitudes   assumed to be in descending order of amplitude.
     */
    private HashMap<String, Double> makeSemitoneAmplitudes(Vector<PitchAmplitude> pitchAmplitudes) {
        // Get total amplitudes for each semitone
        HashMap<String, Double> semitoneAmplitudes = new HashMap<String, Double>();
        for (int i = 0; i < pitchAmplitudes.size(); i++) {
            PitchAmplitude pa = pitchAmplitudes.get(i);
            String semitone = pa.pitch.getSemitone(! useFlats);
            Double oldValue = semitoneAmplitudes.get(semitone);
            if (oldValue == null) {
                semitoneAmplitudes.put(semitone, new Double(pa.amplitude));
            } else {
                Double newValue = new Double(oldValue.doubleValue() + pa.amplitude);
                semitoneAmplitudes.put(semitone, new Double(newValue));
            }
        }
        return semitoneAmplitudes;
    }

    /**
     * Converts a list of (pitch, amplitude) pairs to a set of (text, size) pairs,
     * without combining any pitches. 
     *
     * @param pitchAmplitudes   assumed to be in descending order of amplitude.
     */
    private HashMap<String, Double> makePitchAmplitudes(Vector<PitchAmplitude> pitchAmplitudes) {
        HashMap<String, Double> paHash = new HashMap<String, Double>();
        for (int i = 0; i < pitchAmplitudes.size(); i++) {
             PitchAmplitude pa = pitchAmplitudes.get(i);
             paHash.put(pa.pitch.toString(), new Double(pa.amplitude));
        }
        return paHash; 
    }

    /**
     * Displays the list of text in descending order of size in the given text pane.
     *
     * @param textPane      the text pane in which to display.
     * @param textAndSizes  a mapping of each text item to be displayed to its size, where
     *                      sizes are relative to each other. Displayed font sizes will be
     *                      scaled so that the text with the maximum given size has the
     *                      maximum font size.
     */
    private void setDisplayText(JTextPane textPane, final HashMap<String, Double> textAndSizes) {
        // Sort text in dexcending order of size
        Vector<String> sortedText = new Vector<String>(textAndSizes.keySet());
        Collections.sort(sortedText, new Comparator<String>() {
            public int compare(String s1, String s2) {
                return -textAndSizes.get(s1).compareTo(textAndSizes.get(s2));
            }
        });

        // Figure out text and font sizes for text pane, and set text
        String s = "";
        int[] sizeValues = new int[sortedText.size()];      // sequence of font sizes to apply to s
        int[] sizeIndices = new int[sortedText.size()];     // sequence of indices in s where new font size begins
        int numSizeValues = sizeValues.length;           // number of abs. size values > min font size
        double maxRelSize = textAndSizes.get(sortedText.get(0)).doubleValue();
        for (int i = 0; i < sortedText.size(); i++) {
            String currText = sortedText.get(i);
            double currRelSize = textAndSizes.get(currText).doubleValue();
            int currAbsSize = (int) Math.round(MAX_FONT_SIZE * currRelSize / maxRelSize);
            if (currAbsSize < MIN_FONT_SIZE) {
                numSizeValues = i;
                sizeValues[i] = DOT_FONT_SIZE;
                sizeIndices[i] = s.length();
                s += "...";
                break;
            }
            sizeValues[i] = currAbsSize; 
            sizeIndices[i] = s.length();
            s += currText + " ";
        }
        textPane.setText(s);

        // Set font sizes
        // http://javatechniques.com/blog/setting-jtextpane-font-and-color/
        // http://stackoverflow.com/questions/939109/increasing-the-font-size-of-a-jtextpane-that-displays-html-text
        MutableAttributeSet attrs = textPane.getInputAttributes();
        StyledDocument doc = textPane.getStyledDocument();
        int start, length;
        for (int i = 0; i < numSizeValues - 1; i++) {
            StyleConstants.setFontSize(attrs, sizeValues[i]);
            start = sizeIndices[i];
            length = sizeIndices[i + 1] - sizeIndices[i];
            doc.setCharacterAttributes(start, length, attrs, false);
        }
        StyleConstants.setFontSize(attrs, sizeValues[numSizeValues - 1]);
        start = sizeIndices[numSizeValues - 1];
        length = s.length() - sizeIndices[numSizeValues - 1];
        doc.setCharacterAttributes(start, length, attrs, false);
    }

    private class NoWrapTextPane extends JTextPane {
        public NoWrapTextPane() {
            super();
        }

        // http://objectmix.com/java/73385-disabling-word-wrap-jtextpane.html
        @Override
        public boolean getScrollableTracksViewportWidth() {
            return false;
        }
    }

}
